package com.gizwits.datapoint

import spray.json._
import scala.collection.mutable.BitSet
import Bytes._

object PropType extends Enumeration {
  val Bool = Value("bool")
  val Array = Value("array")
  val BitSet = Value("BitSet")
  val String = Value("string")
  val Hex = Value("hex")
  val Int = Value("int")
  val UInt8 = Value("UInt8")
  val UInt16 = Value("UInt16")
  val UInt32 = Value("UInt32")
  val P0 = Value("p0")
}

case class ProtoDef(target: String, header: List[Byte], config: Option[Map[String, Any]], commands: Map[Symbol, Command]) {
}

case class Command(cmd: List[Byte], udp: Option[Boolean], props: Option[List[Property]]) {
}

trait ReadWrite {
  def isOptional: Boolean
  def getName: Symbol
  def isDynamic: Boolean
  def getPropType: PropType.Value
  def getFixedLength: Int
  def getSubProps: List[NameType] = Nil

  def readInt(buffer: java.nio.ByteBuffer, length: Int): Option[(Symbol, Any)] = {
    if (isOptional && buffer.position == buffer.capacity) None else {
      length match {
        case 1 => Some(getName, (buffer.get & 0xff).toInt)
        case 2 => Some(getName, (buffer.getShort & 0xffff).toInt)
        case 4 => Some(getName, buffer.getInt)
        case _ => None
      }
    }
  }

  def readBitSet(buffer: java.nio.ByteBuffer): Option[(Symbol, Any)] = {
    val byteLength = Math.ceil(getFixedLength / 8.0).toInt
    if (isOptional && buffer.position == buffer.capacity) None else {
      val bytes = new Array[Byte](byteLength)
      buffer.get(bytes)
      val bits = Bits.bytesToBitSet(bytes)
      Some(getName, getSubProps.zipWithIndex.map({case (p, i) =>
        (p.getName, bits(i))
      }).toMap)
    }
  }

  def readLong(buffer: java.nio.ByteBuffer): Option[(Symbol, Any)] = {
    if (isOptional && buffer.position == buffer.capacity) None else {
      getFixedLength match {
        case 1 => Some(getName, buffer.get)
        case 2 => Some(getName, buffer.getShort)
        case 4 => Some(getName, buffer.getInt)
        case 8 => Some(getName, buffer.getLong)
        case _ => None
      }
    }
  }

  def readP0(buffer: java.nio.ByteBuffer): Option[(Symbol, Any)] = {
    if (buffer.hasRemaining) {
      val bytes = new Array[Byte](buffer.remaining)
      buffer.get(bytes)
      Some(getName, bytes.toList)
    } else None
  }

  def readStr(buffer: java.nio.ByteBuffer, hex: Boolean = false): Option[(Symbol, Any)] = {
    val length = if (isOptional && buffer.position == buffer.capacity) 0 else if (isDynamic) buffer.getShort else if (getFixedLength > 0) getFixedLength else -1
    if (length > 0) {
      val arr = new Array[Byte](if(hex) length/2 else length)
      buffer.get(arr)
      val value = if (hex) bytes2hex(arr.toList) else new String(arr.filter(_ != 0))
      Some(getName, value)
    } else None
  }

  def read(buffer: java.nio.ByteBuffer, pos: Int): Option[(Symbol, Any)]= {
    getPropType match {
      case PropType.String => readStr(buffer)
      case PropType.Hex => readStr(buffer, true)
      case PropType.UInt8 => readInt(buffer, 1)
      case PropType.UInt16 => readInt(buffer, 2)
      case PropType.Int => readLong(buffer)
      case PropType.Array => readArray(buffer)
      case PropType.P0 => readP0(buffer)
      case PropType.BitSet => readBitSet(buffer)
      case _ => None
    }
  }

  def readArray(buffer: java.nio.ByteBuffer): Option[(Symbol, Any)] = {
    if (isOptional && buffer.position == buffer.capacity) None else {
      def loopRead(): Stream[Map[Symbol, Any]] = (buffer.position == buffer.capacity) match {
        case true => Stream.empty
        case false => {
          getSubProps.zipWithIndex.map({case (p, i) =>
            p match {
              case subProp: NameType => subProp.read(buffer, buffer.position)
              case _ => None
            }
          }).filter(!_.isEmpty).map(_.get).toMap
        } #:: loopRead()
      }
      Some(getName, loopRead().toList)
    }
  }
  def writeStr(str: String): List[Byte] = {
    if (getFixedLength > 0) {
      val buffer = java.nio.ByteBuffer.allocate(getFixedLength)
      str.getBytes.slice(0, getFixedLength).foreach(buffer.put(_))
      buffer.array.toList
    } else {
      val buffer = java.nio.ByteBuffer.allocate(2 + str.length)
      buffer.putShort(str.length.toShort)
      str.getBytes.foreach(buffer.put(_))
      buffer.array.toList
    }
  }

  def writeInt(value: Int, length: Int): List[Byte] = {
    val buffer = java.nio.ByteBuffer.allocate(length)
    length match {
      case 1 => buffer.put(value.toByte)
      case 2 => buffer.putShort(value.toShort)
      case 4 => buffer.putInt(value)
      case _ => return Nil
    }
    buffer.array.toList
  }

  def writeLong(value: Long): List[Byte] = {
    val buffer = java.nio.ByteBuffer.allocate(getFixedLength)
    getFixedLength match {
      case 1 => buffer.put(value.toByte)
      case 2 => buffer.putShort(value.toShort)
      case 4 => buffer.putInt(value.toInt)
      case 8 => buffer.putLong(value)
      case _ => return Nil
    }
    buffer.array.toList
  }

  def writeHex(str: String): List[Byte] = {
    val buffer = java.nio.ByteBuffer.allocate(2 + str.length/2)
    buffer.putShort(str.length.toShort)
    hex2bytes(str).foreach(buffer.put(_))
    buffer.array.toList
  }

  def writeBitSet(values: Map[Symbol, Boolean]): List[Byte] = {
    val bytes = Math.ceil(getFixedLength / 8.0).toInt
    val rightIndex = bytes * 8 - 1
    val bits = new BitSet()
    getSubProps.zipWithIndex.map({case (p, i) =>
      bits(i) = (values.contains(p.getName) && values(p.getName))
    })
    Bits.bitSetToBytes(bits, bytes).toList
  }

  def writeArray(arr: List[Any]): List[Byte] = {
    def writeElement(ele: Any): List[Byte] = {
      getSubProps.map(p =>
          p match {
            case subProp: NameType => {
              val m = ele.asInstanceOf[Map[Symbol, Any]]
              val value = m(subProp.getName)
              subProp.write(value)
            }
            case _ => Nil
          }
          ).flatten.toList
    }
    arr.map(writeElement).flatten.toList
  }

  def write(value: Any): List[Byte] = {
    getPropType match {
      case PropType.String => value match {
        case str: String => writeStr(str)
        case _ => Nil
      }
      case PropType.Hex => value match {
        case str: String => writeHex(str)
        case _ => Nil
      }
      case PropType.UInt8 => value match {
        case int: Int => writeInt(int, 1)
        case _ => Nil
      }
      case PropType.UInt16 => value match {
        case int: Int => writeInt(int, 2)
        case _ => Nil
      }
      case PropType.Int => value match {
        case dbl: Long => writeLong(dbl)
        case _ => Nil
      }
      case PropType.Array => writeArray(value.asInstanceOf[List[Any]])
      case PropType.P0 => value.asInstanceOf[List[Byte]]
      case PropType.BitSet => writeBitSet(value.asInstanceOf[Map[Symbol, Boolean]])
      case _ => Nil
    }
  }
}

            case class NameType(name: Symbol, propType: PropType.Value, dynamic: Option[Boolean]) extends ReadWrite {
              override def isOptional: Boolean = false
              override def getName: Symbol = name
              override def isDynamic: Boolean = dynamic getOrElse false
              override def getPropType: PropType.Value = propType
              override def getFixedLength: Int = -1
            }


            case class Property(name: Symbol, propType: PropType.Value, dynamic: Option[Boolean], optional: Option[Boolean], subProps: Option[List[NameType]], fixedLength: Option[Int]) extends ReadWrite {
              override def isOptional: Boolean = optional getOrElse false
              override def getName: Symbol = name
              override def isDynamic: Boolean = dynamic getOrElse false
              override def getPropType: PropType.Value = propType
              override def getFixedLength: Int = fixedLength getOrElse -1
              override def getSubProps: List[NameType] = subProps getOrElse Nil
            }

            object ProtoDefJsonProtocol extends DefaultJsonProtocol with NullOptions {
              implicit object AnyJsonFormat extends JsonFormat[Any] {
                def write(x: Any): JsValue = x match {
                  case n: Int => JsNumber(n)
                  case l: Long => JsNumber(l)
                  case d: Double => JsNumber(d)
                  case f: Float => JsNumber(f.toDouble)
                  case s: String => JsString(s)
                  case x: Seq[_] => seqFormat[Any].write(x)
                  case m: Map[_, _] if m.isEmpty => JsObject(Map[String, JsValue]())
                  // Get the type of map keys from the first key, translate the rest the same way
                  case m: Map[_, _] => m.keys.head match {
                    case sym: Symbol =>
                      val map = m.asInstanceOf[Map[Symbol, _]]
                      val pairs = map.map { case (sym, v) => (sym.name -> write(v)) }
                      JsObject(pairs)
                    case s: String => mapFormat[String, Any].write(m.asInstanceOf[Map[String, Any]])
                    case a: Any =>
                      val map = m.asInstanceOf[Map[Any, _]]
                      val pairs = map.map { case (sym, v) => (sym.toString -> write(v)) }
                      JsObject(pairs)
                  }
                    case a: Array[_] => seqFormat[Any].write(a.toSeq)
                    case true        => JsTrue
                    case false       => JsFalse
                    case p: Product  => seqFormat[Any].write(p.productIterator.toSeq)
                    case null        => JsNull
                    case x           => JsString(x.toString)
                }
                def read(value: JsValue): Any = {
                  value match {
                    case JsNumber(n) => n.intValue()
                    case JsString(s) => s
                    case a: JsArray => listFormat[Any].read(value)
                    case o: JsObject => mapFormat[String, Any].read(value)
                    case JsTrue => true
                    case JsFalse => false
                    case x => deserializationError("Do not understand how to deserialize " + x)
                }}
              }
              implicit object ListByteFormat extends JsonFormat[List[Byte]] {
                def write(bytes: List[Byte]): JsValue = {
                  JsString(bytes2hex(bytes))
                }
                def read(value: JsValue) = value match {
                  case JsString(s) => hex2bytes(s.substring(2))
                  case x => deserializationError("Do not understand how to deserialize " + x)
                }
              }
              implicit val propTypeFormat = DataPointJsonProtocol.jsonEnum(PropType)
              implicit val nameTypeFormat = jsonFormat(NameType.apply, "name", "type", "dynamic")
              implicit val propFormat = jsonFormat(Property.apply, "name", "type", "dynamic", "optional", "props", "fixed_length")
              implicit val cmdFormat = jsonFormat3(Command.apply)
              implicit val protoDefFormat: RootJsonFormat[ProtoDef] = jsonFormat4(ProtoDef.apply)
            }

            object ProtoDef {
              def apply(jsonStr: String): ProtoDef = {
                val json = jsonStr.parseJson
                import ProtoDefJsonProtocol._
                val pd = json.convertTo[ProtoDef]
                pd
              }
            }
